﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Xml.XPath;

namespace Quail
{
    /// <summary>
    /// Implementation of IWebPage using the BasicWebBrowser and BasicTestShell.  It supports the concept of Xml and Json response types
    /// </summary>
    public class BasicWebPage : IWebPage, IXmlNode, IDisposable
    {
        private bool _setOnAfterNav;

        /// <summary>
        /// Initializes a new instance of the <see cref="BasicWebPage"/> class.
        /// </summary>
        /// <param name="shell">The Test shell.</param>
        public BasicWebPage(BasicTestShell shell)
        {
            Shell = shell;
            SetAfterNav();
        }

        ~BasicWebPage()
        {
            Dispose(false);
        }

        private void SetAfterNav()
        {
            if (!_setOnAfterNav)
            {
                if (Browser != null)
                {
                    Browser.OnAfterNavigate += OnAfterBrowserNavigate;
                    _setOnAfterNav = true;
                }
            }
        }

        private void OnAfterBrowserNavigate(BasicWebRequest webrequest, HttpResponseMessage currentresponse)
        {
            if (OnClearCache != null)
                OnClearCache();
        }

        /// <summary>
        /// Called when the page is no longer needed
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_setOnAfterNav && Browser != null)
                {
                    _setOnAfterNav = false;
                    Browser.OnAfterNavigate -= OnAfterBrowserNavigate;
                }                
            }
        }

        /// <summary>
        /// Gets the page.
        /// </summary>
        /// <value>The page.</value>
        public BasicWebPage Page
        {
            get { return this; }
        }

        /// <summary>
        /// Gets the BasicWebBrowser
        /// </summary>
        public BasicWebBrowser Browser { get { return Shell.Browser; } }

        /// <summary>
        /// Gets the Shell.
        /// </summary>
        public BasicTestShell Shell { get; private set; }


        /// <summary>
        /// Waits for this page to finish loading.  If the page is already done loading, it won't wait.  It will also check for a YSOD based on the YSODCheckType
        /// </summary>
        /// <param name="ysod"></param>
        public void Sync(YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            Sync(ysod, LogStatus.FailYsod);
        }

        /// <summary>
        /// Waits for this page to finish loading.  If the page is already done loading, it won't wait.  It will also check for a YSOD based on the YSODCheckType
        /// </summary>
        /// <param name="ysod"></param>
        /// <param name="ysodStatus"></param>
        public void Sync(YSODCheckTypes ysod, LogStatus ysodStatus)
        {
            try
            {
                PerformSync(ysod, ysodStatus, this);
                if (OnSyncIsCompleteEvent != null)
                    OnSyncIsCompleteEvent(this);
            }
            catch (Exception e)
            {
                if (OnSyncFailedEvent != null)
                    OnSyncFailedEvent(this, e);
                throw;
            }
        }

        /// <summary>
        /// Waits for any page to finish loading.  If the page is already done loading, it won't wait.  It will also check for a YSOD based on the YSODCheckType
        /// </summary>
        /// <param name="ysod"></param>
        public void SyncAnyPage(YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            SyncAnyPage(ysod, LogStatus.FailYsod);
        }

        /// <summary>
        /// Waits for any page to finish loading.  If the page is already done loading, it won't wait.  It will also check for a YSOD based on the YSODCheckType
        /// </summary>
        /// <param name="ysod"></param>
        /// <param name="ysodStatus"></param>
        public void SyncAnyPage(YSODCheckTypes ysod, LogStatus ysodStatus)
        {
            try
            {
                PerformSync(ysod, ysodStatus);
                if (OnSyncIsCompleteEvent != null)
                    OnSyncIsCompleteEvent(this);
            }
            catch (Exception e)
            {
                if (OnSyncFailedEvent != null)
                    OnSyncFailedEvent(this, e);
                throw;
            }
        }

        protected void PerformSync(YSODCheckTypes checkForYSOD, LogStatus ysodStatus, params BasicWebPage[] pages)
        {
            var task = Browser.ActiveTask.BeginTask("BasicWebPage.PerformSync");
            try
            {
                if (Browser.RequestPending)
                {
                    task.AddAttribute("wait", "True");

                    // waitForPageToLoadIfPageIsLoading seems to suck...
                    // Browser.DoQuick(() => Browser.DoCommand("waitForPageToLoadIfPageIsLoading", Browser.TimeoutString));

                    // let's try polling instead
                    var wait = new WaitParameters("Sync")
                                   {
                                       Duration = Browser.Timeout,
                                       PollingInterval = 500,
                                       Delegate = () => !Browser.RequestPending,
                                       FailStatus = LogStatus.FailSync
                                   };
                    wait.Wait(ActiveTask);
                }

                task.AddLog(LogStatus.Navigate);
                if (checkForYSOD == YSODCheckTypes.CheckForYSOD || checkForYSOD == YSODCheckTypes.CheckForYSODButDontThrow)
                {
                    if (HasYSOD(ysodStatus))
                    {
                        var request = Browser.RawRequest;
                        request.Log(ysodStatus, task);
                        task.AddLog(ysodStatus, Html);

                        if (checkForYSOD == YSODCheckTypes.CheckForYSOD)
                            throw new YSODFoundException(this, Browser.Location);

                        task.Finish(ysodStatus);
                        return;
                    }
                }

                if (pages != null && pages.Length > 0)
                {
                    task.AddAttribute("mustexist", "True");

                    var found = pages.Any(page => page.Exists);

                    if (!found)
                    {
                        throw new WebPageNotFoundException(this, "No expected pages were found!");
                    }
                }

                task.Finish();
            }
            catch (YSODFoundException y)
            {
                task.Finish(ysodStatus, y);
                throw;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }            
        }

        /// <summary>
        /// Returns TRUE if the page contains an error (aka YSOD)
        /// </summary>
        /// <returns>True if there's an error on the page</returns>
        public virtual bool HasYSOD(LogStatus ysodStatus = LogStatus.FailYsod, string message = null)
        {
            return
                ActiveTask
                    .RunTask(
                        task =>
                            {
                                var code = Browser.CurrentStatusCode;

                                if (code.HasValue)
                                {
                                    task.AddAttribute("Status Code", code.Value);

                                    if (code.Value == HttpStatusCode.NotFound)
                                        return true;
                                    if (code.Value != HttpStatusCode.OK)
                                        return true;
                                }

                                // search the HTML for matches
                                if (_ysodItems == null)
                                    _ysodItems = GetYSODSearchItems().ToList();

                                if (!_ysodItems.Any())
                                    return false;

                                var html = Html;
                                var htmllower = html.ToLower();
                                var logmessage = string.Empty;
                                var foundysod = false;

                                foreach (var ysod in _ysodItems
                                    .Where(ysod => htmllower.Contains(ysod.TriggerTextLower)))
                                {
                                    logmessage = ysod.Message + " found";

                                    if (string.IsNullOrEmpty(message))
                                        logmessage += "\n" + html;
                                    else
                                        logmessage += ": " + message + "\n" + html;

                                    foundysod = true;
                                    break;
                                }

                                if (foundysod)
                                {
                                    task.Finish(ysodStatus, logmessage);
                                    return true;
                                }

                                task.Rollback();

                                return false;
                            },
                        false
                    );
        }

        private List<YSODSearchItem> _ysodItems;

        protected virtual IEnumerable<YSODSearchItem> GetYSODSearchItems()
        {
            return YSODSearchItem.GenericYSODSearchItems();
        }

        /// <summary>
        /// Called when the Sync() method completes successfully
        /// </summary>
		public event OnWebPageCompleteEvent OnSyncIsCompleteEvent;

        /// <summary>
        /// Called when the Sync() method fails
        /// </summary>
        public event OnFailedEvent OnSyncFailedEvent;

        /// <summary>
        /// Returns the location of the page (what would display in a user's address bar)
        /// </summary>
        public string Location
        {
            get { return Browser.Location; }
        }

        public string SafeLocation
        {
            get { return Location; }
        }

        /// <summary>
        /// Returns the Shell's active task
        /// </summary>
        public Task ActiveTask
        {
            get { return Shell.ActiveTask; }
        }

        /// <summary>
        /// Gets the parent node.
        /// </summary>
        /// <value>The parent node.</value>
        public IXmlNode ParentNode
        {
            get { return null; }
        }

        /// <summary>
        /// Event fired when the page should clear its cache
        /// </summary>
        public event OnClearCacheEvent OnClearCache;

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public IXmlNode GetNode(string selector)
        {
            return new BasicXmlNode(this, Browser.GetNode(selector), selector);
        }

        /// <summary>
        /// Get a node from the XML based on the given selector
        /// </summary>
        /// <param name="selector">The XML selector</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public IEnumerable<IXmlNode> GetNodes(string selector)
        {
            return Browser.GetNodes(selector)
                .Select(n => new BasicXmlNode(this, n, selector));
        }

        /// <summary>
        /// Get a list of items from the XML based on the given selector
        /// </summary>
        /// <param name="selector">The XML selector</param>
        /// <param name="creator">Creator method</param>
        /// <returns>The node matching the given selector in the XML</returns>
        public List<T> GetList<T>(string selector, Func<XPathNavigator, string, T> creator)
        {
            return Browser.GetNodes(selector)
                .Select(n => creator(n,selector))
                .ToList();
        }

        /// <summary>
        /// Get a value from the XML based on the given selector
        /// </summary>
        /// <typeparam name="T">The type of the value to get</typeparam>
        /// <param name="selector">The XML selector</param>
        /// <returns>The value of the given selector in the XML</returns>
        public T GetValue<T>(string selector)
        {
            return Browser.GetValue<T>(selector);
        }

        /// <summary>
        /// Get a value from the XML of the given node
        /// </summary>
        /// <typeparam name="T">The type of the value to get - string, int, decimal or bool</typeparam>
        /// <returns>The value of the given selector in the XML</returns>
        public T GetValue<T>()
        {
            return default(T);
        }

        /// <summary>
        /// Get a list of string values found with the given xml element name
        /// </summary>
        /// <param name="selector">The XPath selector</param>
        /// <returns>The list of strings</returns>
        public List<T> GetValues<T>(string selector)
        {
            return Browser.GetValues<T>(Browser.XmlRoot, selector);
        }

        /// <summary>
        /// Returns True if this page exists (default implementation returns true every time)
        /// </summary>
        public virtual bool Exists
        {
            get { return true; }
        }

        /// <summary>
        /// Gets or sets the selector.
        /// </summary>
        /// <value>The selector.</value>
        public string Selector
        {
            get { return string.Empty; }
            set { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Returns this page's HTML
        /// </summary>
        public string Html
        {
            get
            {
                return Browser.GetCurrentResponse();
            }
        }
    }
}
